跳到主要内容

MySQL 分布式事务

什么是分布式事务?

就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

分布式事务的产生的原因

数据库分库分表

当数据库单表一年产生的数据超过 1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问 01 库,又访问 02 库,而且要保证数据的一致性,那么就要用到分布式事务。

应用 SOA 化

所谓的 SOA 化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。

对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。

以上两种情况表象不同,但是本质相同,都是因为要操作的数据库变多了!

分布式事务的应用场景

支付

最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。

而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。

在线下单

买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。

常见的分布式事务解决方案

实现分布式事务的方案比较多,常见的比如基于 XA 协议的 2PC、3PC,基于业务层的 TCC,还有应用消息队列 + 消息表实现的最终一致性方案

2PC

基于 XA 协议实现的分布式事务,XA 协议中分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如 Oracle、MYSQL 这些数据库都实现了 XA 接口,而事务管理器则作为一个全局的调度者。

两阶段提交(2PC),对业务侵⼊很小,它最⼤的优势就是对使⽤⽅透明,用户可以像使⽤本地事务⼀样使⽤基于 XA 协议的分布式事务,能够严格保障事务 ACID 特性。

可 2PC的缺点也是显而易见,它是一个强一致性的同步阻塞协议,事务执⾏过程中需要将所需资源全部锁定,也就是俗称的 刚性事务。所以它比较适⽤于执⾏时间确定的短事务,整体性能比较差。

一旦事务协调者宕机或者发生网络抖动,会让参与者一直处于锁定资源的状态或者只有一部分参与者提交成功,导致数据的不一致。因此,在⾼并发性能⾄上的场景中,基于 XA 协议的分布式事务并不是最佳选择。

3PC

三段提交(3PC)是二阶段提交(2PC)的一种改进版本 ,为解决两阶段提交协议的阻塞问题,上边提到两段提交,当协调者崩溃时,参与者不能做出最后的选择,就会一直保持阻塞锁定资源。

2PC 中只有协调者有超时机制,3PC 在协调者和参与者中都引入了超时机制,协调者出现故障后,参与者就不会一直阻塞。而且 在第一阶段和第二阶段中又插入了一个准备阶段(如下图,看着有点啰嗦),保证了在最后提交阶段之前各参与节点的状态是一致的。

虽然 3PC 用超时机制,解决了协调者故障后参与者的阻塞问题,但与此同时却多了一次网络通信,性能上反而变得更差,也不太推荐。

TCC 编程模式

所谓的 TCC 编程模式,也是两阶段提交的一个变种,不同的是 TCC 为在业务层编写代码实现的两阶段提交。 TCC 分别指 Try、Confirm、Cancel ,一个业务操作要对应的写这三个方法。

  • Try 阶段主要是对 业务系统做检测及资源预留
  • Confirm 阶段主要是对 业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放

比如下一个订单减一个库存:

执行流程:

Try 阶段:订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于 1,然后将可用库存数量设置为库存剩余数量 -1

  1. 如果 Try 阶段 执行成功,执行 Confirm 阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量
  2. 如果 Try 阶段 执行失败,执行 Cancel 阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量

TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的补偿性事务。

原本一个方法,现在却需要三个方法来支持,可以看到 TCC 对业务的侵入性很强,而且这种模式并不能很好地被复用,会导致开发量激增。还要考虑到网络波动等原因,为保证请求一定送达都会有重试机制,所以考虑到接口的幂等性。

消息事务(最终一致性)

所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的 RocketMQ 就支持这一特性,具体原理如下:

1、A 系统向消息中间件发送一条预备消息 2、消息中间件保存预备消息并返回成功 3、A 执行本地事务 4、A 发送提交消息给消息中间件

通过以上 4 步完成了一个消息事务。对于以上的 4 个步骤,每个步骤都可能产生错误,下面一一分析:

步骤一出错,则整个事务失败,不会执行 A 的本地操作;

步骤二出错,则整个事务失败,不会执行 A 的本地操作;

步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是 A 系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查 A 事务执行是否执行成功,如果失败则回滚预备消息;

步骤四出错,这时候 A 的本地事务是成功的,那么消息中间件要回滚 A 吗?答案是不需要,其实通过回调接口,消息中间件能够检查到 A 执行成功了,这时候其实不需要 A 发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务。

基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A 系统的本地操作 + 发消息)+B 系统的本地操作,其中 B 系统的操作由消息驱动,只要消息事务成功,那么 A 操作一定成功,消息也一定发出来了,这时候 B 会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到 B 操作成功,这样就变相地实现了 A 与 B 的分布式事务。原理如下:

虽然上面的方案能够完成 A 和 B 的操作,但是 A 和 B 并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。

基于消息中间件的两阶段提交方案,通常用在高并发场景下使用,牺牲数据的强一致性换取性能的大幅提升,不过实现这种方式的成本和复杂度是比较高的,还要看实际业务情况。

Saga 事务模式

Saga 事务模型又叫做长时间运行的事务(这里使用 DTM 这个工具去做这个协调器)

其核心思想是 将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果 某个步骤失败,则根据相反顺序一次调用补偿操作

例如我们要进行一个类似于银行跨行转账的业务,将 A 中的 30 元转给 B,根据 Saga 事务的原理,我们将整个全局事务,切分为以下服务:

  • 转出(TransOut)服务,这里转出将会进行操作A-30
  • 转出补偿(TransOutCompensate)服务,回滚上面的转出操作,即A+30
  • 转入(TransIn)服务,转入将会进行B+30
  • 转入补偿(TransInCompensate)服务,回滚上面的转入操作,即B-30

整个 Saga 事务的逻辑是:

执行转出成功 => 执行转入成功 => 全局事务完成

如果在中间发生错误,例如转入 B 发生错误,则会调用已执行分支的补偿操作,即:

执行转出成功 => 执行转入失败 => 执行转入补偿成功 => 执行转出补偿成功 => 全局事务回滚完成

下面我们看一个成功完成的 SAGA 事务典型的时序图:

在这个图中,我们的全局事务发起人,将整个全局事务的编排信息,包括每个步骤的正向操作和反向补偿操作定义好之后,提交给服务器,服务器就会按步骤执行前面 SAGA 的逻辑。

失败的时序图如下:

总结

分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务 + 最终一致性、TCC 模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。作为技术人员,一定不能忘了技术是为业务服务的,不要为了技术而技术,针对不同业务进行技术选型也是一种很重要的能力!

代码实现

https://dtm.pub/guide/why.html

References